
local imageio = require 'imageio'
local bit = require 'bit'
local struct = require 'alien.struct'
local tostring, assert, ipairs, setmetatable, unpack, error
    = tostring, assert, ipairs, setmetatable, unpack, error
local tconcat
    = table.concat
local strbyte
    = string.byte

setfenv(1, imageio)

-- filestruct

local CHAR = 'B'
local INT = 'i4'
local UINT = 'I4'
local SHORT = 'i2'
local USHORT = 'I2'

local filestruct_proto = {}
local filestruct_meta = {__index = filestruct_proto}

local function filestruct(def)
  local pattern = {'<'}
  for i, v in ipairs(def) do
    pattern[i+1] = v[2]
  end
  pattern = tconcat(pattern)
  def.pattern = pattern
  def.size = struct.size(pattern)
  return setmetatable(def, filestruct_meta)
end

function filestruct_proto:read(f)
  local size = self.size
  local data = f:read(size)
  if (data == nil) or (#data ~= size) then
    return nil
  end
  print(self.pattern, self.size)
  local values = {struct.unpack(self.pattern, data)}
  for i = 1,#self do
    values[self[i][1]] = values[i]
  end
  return values
end

function filestruct_proto:tostring(values)
  local arr = {}
  for i,v in ipairs(self) do
    arr[#arr+1] = assert(values[v[1]], "missing field: " .. v[1])
  end
  return struct.pack(self.pattern, unpack(arr))
end

-- end of filestruct

local logical_screen_descriptor = filestruct{
  {"canvas_width", USHORT};
  {"canvas_height", USHORT};
  {"packed_field", CHAR};
  {"bg_color", CHAR};
  {"pixel_aspect_ratio", CHAR};
}

local image_descriptor = filestruct{
  {"left", USHORT};
  {"top", USHORT};
  {"width", USHORT};
  {"height", USHORT};
  {"packed_field", CHAR};
}

local TRAILER = ';'

function skip_block(f)
  local count = strbyte(assert(f:read(1)))
  if (count == 0) then
    return false
  else
    f:seek('cur', count)
    return true
  end
end

function get_block(f)
  local count = strbyte(assert(f:read(1)))
  if (count == 0) then
    return nil
  else
    local block = f:read(count)
    assert(block and (#block == count), "unexpected end of file")
    return block
  end
end

function read_whole_block(f, bytes)
  local blocks
  while true do
    local block = assert(get_block(f), "unexpected end of file")
    if blocks then
      blocks[#blocks+1] = block
      bytes = bytes - #block
      if (bytes == 0) then
        assert(f:read(1) == '\0', "too much data")
        return tconcat(blocks)
      end
      assert(bytes > 0, "too much data")
    else
      if (#block == bytes) then
        assert(f:read(1) == '\0', "too much data")
        return block
      elseif (#block > bytes) then
        error("too much data")
      else
        blocks = {block}
        bytes = bytes - #block
      end
    end
  end
end

imageio.add_handler{

  pattern = 'GIF8[79]a';
  
  reader = function(f)
    local signature = f:read(6) -- GIF87a or GIF89a
    
    local screen_info = assert(logical_screen_descriptor:read(f), 'unexpected end of file')
    
    local packed = screen_info.packed_field
    
    local global_color_table_flag = bit.band(bit.rshift(packed, 7), 1)
    local color_resolution = bit.band(bit.rshift(packed, 4), 7)
    local sort_flag = bit.band(bit.rshift(packed, 3), 1)
    local global_color_table_size = bit.band(packed, 7)
    
    local global_palette
    if (global_color_table_flag == 1) then
      global_palette = {}
      local color_count = 2^(1+global_color_table_size)
      for i = 1, color_count do
        local r,g,b = strbyte(f:read(3), 1, 3)
        global_palette[i] = {r=r/255, g=g/255, b=b/255}
      end
    end
    
    local delay, transparent_color, disposal_method
    
    local local_palette
    
    while true do
      local b = f:read(1)
      if (b == ',') then
        -- image descriptor
        local frame_info = image_descriptor:read(f)

        local packed_field = frame_info.packed_field
        local local_color_table_flag = bit.band(bit.rshift(packed_field, 7), 1)
        local interlace_flag = bit.band(bit.rshift(packed_field, 6), 1)
        local sort_flag = bit.band(bit.rshift(packed_field, 5), 1)
        local local_color_table_size = bit.band(packed_field, 7)
        
        if (local_color_table_flag == 1) then
          local_palette = {}
          for i = 1, 2^(1+local_color_table_size) do
            local r,g,b = strbyte(f:read(3), 1, 3)
            local_palette[i] = {r=r/255, g=g/255, b=b/255}
          end
        end
        
        -- now the actual image data starts, so we stop
        break
      elseif (b == '!') then
        -- extension
        local ext_type = assert(f:read(1), "unexpected end of file")
        if (ext_type == '\249') then
          -- graphics control extension
          local packed_field, delay_time, transparent_color_index = struct.unpack('<BI2B', read_whole_block(f, 4))
          
          disposal_method = bit.band(bit.rshift(packed_field, 2), 7)
          local user_input_flag = bit.band(bit.rshift(packed_field, 1), 1)
          local transparent_color_flag = bit.band(packed_field, 1)
          
          delay = delay_time
          transparent_color = (transparent_color_flag == 1) and transparent_color_index or nil
          disposal_method = disp
        else
          -- ignore whatever extension this is
          repeat until (skip_block(f) == false);
        end
      elseif (b == ';') or (b == nil) then
        -- trailer
        error('unexpected end of data')
      end
    end
    
    local color_table = {}
    for i,v in ipairs(palette) do
      color_table[i] = strchar(i)
    end
    local clear_code = strchar(#palette)
    local eoi_code = strchar(#palette+1)
    color_table[#color_table+1] = clear_code
    color_table[#color_table+2] = eoi_code
    
    return imageio.create_reader{
      bpp = 8;
      format = "p8";
      palette = palette;
      reader = reader;
      
      width = screen_info.canvas_width;
      height = screen_info.canvas_height;
      pitch = info.rowbytes;
      
      getdata = cowrap(function(self)
        local buf = {}
        local buf_count = 0
        local bytes = 0
        while true do
          local block = get_block(f)
          local ptr = 1
          local last_c
          while (ptr < #block) do
            local c = strsub(block,ptr,ptr)
            if (c == clear_code) then
            elseif (c == eoi_code) then
            end
          end
          yield(tconcat(buf,i,buf_count))
          buf_count = 0
          bytes = 0
        end
      end);
    }
  end;
  
}

imageio.add_saver{
  name = 'gif';
  extensions = {'gif'};
  save = function(pixmap, f)
    
    f:write("GIF89a")
    
    local palette = assert(pixmap.palette, "gif image must be paletted")
    
    local global_color_table_flag = 1
    local color_resolution = 0
    local sort_flag = 0
    local global_color_table_size = 1
    local global_packed = bit.bor(
      bit.lshift(global_color_table_flag, 7),
      bit.lshift(color_resolution, 4),
      bit.lshift(sort_flag, 3),
      global_color_table_size
    )
    
    f:write(logical_screen_descriptor:tostring{
      width = pixmap.width;
      height = pixmap.height;
      packed_field = global_packed;
      bg_color = 0;
      pixel_aspect_ratio = 0;
    })
    
    local local_color_table_flag = 0
    local interlace_flag = 0
    local sort_flag = 0
    local local_color_table_size = 0
    local local_packed = bit.bor(
      bit.lshift(local_color_table_flag, 7),
      bit.lshift(interface_flag, 6),
      bit.lshift(sort_flag, 5),
      local_color_table_size
    )
    
    f:write(image_descriptor:tostring{
      left = 0;
      top = 0;
      width = pixmap.width;
      height = pixmap.height;
      packed_field = local_packed;
    })
    
    f:write(';') -- trailer
    
  end;
}
